﻿#include "Shader.hpp"
#include "SOIL.h"
#include <iostream>

namespace phx
{

Shader::Shader() : 
	mVao(0),
	mVbo(0),
	mEbo(0),
	mVertexShader(NULL),
	mFragmentShader(NULL)
{

}

bool Shader::loadFromMemory(ShaderCode vertexShaderCode, ShaderCode fragmentShaderCode)
{
	mVertexShader = glCreateShader(GL_VERTEX_SHADER);
	glShaderSource(mVertexShader, 1, &vertexShaderCode, NULL);
	glCompileShader(mVertexShader);
	int vertexShaderCompileStatus = -1;
	glGetShaderiv(mVertexShader, GL_COMPILE_STATUS, &vertexShaderCompileStatus);
	if(vertexShaderCompileStatus != GL_TRUE)
	{
		#ifdef PHX_DEBUG
			std::cerr<<"Vertex shader compilation failed with this message:"<<std::endl;
			const int logLength = 512;
			char log[logLength];
			glGetShaderInfoLog(mVertexShader, logLength, NULL, log);
			std::cerr<<log<<std::endl;
		#endif
		return false;
	}

	mFragmentShader = glCreateShader(GL_FRAGMENT_SHADER);
	glShaderSource(mFragmentShader, 1, &fragmentShaderCode, NULL);
	glCompileShader(mFragmentShader);
	int fragmentShaderCompileStatus = -1;
	glGetShaderiv(mFragmentShader, GL_COMPILE_STATUS, &fragmentShaderCompileStatus);
	if(fragmentShaderCompileStatus != GL_TRUE)
	{
		#ifdef PHX_DEBUG
			std::cerr<<"Fragment shader compilation failed with this message:"<<std::endl;
			const int logLength = 512;
			char log[logLength];
			glGetShaderInfoLog(mFragmentShader, logLength, NULL, log);
			std::cerr<<log<<std::endl;
		#endif
		return false;
	}

	mShaderProgram = glCreateProgram();
	glAttachShader(mShaderProgram, mVertexShader);
	glAttachShader(mShaderProgram, mFragmentShader);
	glBindFragDataLocation(mShaderProgram, 0, "outColor");
	glLinkProgram(mShaderProgram);

	//Check for errors
    GLint programSuccess = GL_TRUE;
    glGetProgramiv( mShaderProgram, GL_LINK_STATUS, &programSuccess );
    if( programSuccess != GL_TRUE )
    {
        #ifdef PHX_DEBUG
			std::cerr<<"Failed to link shaders, failed with error:"<<std::endl;
			const int logLength = 512;
			char log[logLength];
			glGetShaderInfoLog(mShaderProgram, logLength, NULL, log);
			std::cerr<<log<<std::endl;
		#endif
		return false;
    }

	glUseProgram(mShaderProgram);

	glGenVertexArrays(1, &mVao);
	glBindVertexArray(mVao);

	float vertices[] = 
	{
	//  Position       Texcoords
		-1.0f,  -1.0f,	0.0f, 0.0f, // Top-left
		1.0f,	-1.0f,	1.0f, 0.0f, // Top-right
		1.0f,	1.0f,	1.0f, 1.0f, // Bottom-right
		-1.0f,  1.0f,	0.0f, 1.0f  // Bottom-left
	};
	// Create a Vertex Buffer Object that will store the vertices on video memory
	glGenBuffers(1, &mVbo);
	// Allocate space and upload the data from CPU to GPU
	glBindBuffer(GL_ARRAY_BUFFER, mVbo);
	glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);

	glGenBuffers(1, &mEbo);
	GLuint elements[] = 
	{
		1, 0, 3,
		3, 2, 1
	};
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mEbo);
	glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(elements), elements, GL_STATIC_DRAW);

	GLint posAttrib = glGetAttribLocation(mShaderProgram, "position");
	glEnableVertexAttribArray(posAttrib);
	glVertexAttribPointer(posAttrib, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), 0);

	GLint texAttrib = glGetAttribLocation(mShaderProgram, "texcoord");
	glEnableVertexAttribArray(texAttrib);
	glVertexAttribPointer(texAttrib, 2, GL_FLOAT, GL_FALSE, 4*sizeof(float), (void*)(2*sizeof(float)));

	glBindVertexArray(0);
	glBindBuffer(GL_ARRAY_BUFFER, 0);
	glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);

	return true;
}

void Shader::setValue(const char *uniformName, const glm::mat4 &matrix)
{
	GLint uniformLocation = glGetUniformLocation(mShaderProgram, uniformName);
	glUniformMatrix4fv(uniformLocation, 1, GL_FALSE, glm::value_ptr(matrix));
}

void Shader::setValue(const char *uniformName, const glm::vec3 &vec)
{
	GLint uniformLocation = glGetUniformLocation(mShaderProgram, uniformName);
	const glm::mediump_float *v = glm::value_ptr(vec);
	glUniform3f(uniformLocation, v[0], v[1], v[3]);
}

void Shader::setValue(const char *uniformName, const vec2f &vec)
{
	GLint uniformLocation = glGetUniformLocation(mShaderProgram, uniformName);
	glUniform2f(uniformLocation, vec.x, vec.y);
}

void Shader::setValue(const char *uniformName, GLint value)
{
	GLint uniformLocation = glGetUniformLocation(mShaderProgram, uniformName);
	glUniform1i(uniformLocation, value);
}

void Shader::setValue(const char *uniformName, const phx::Colorf &color)
{
	GLint uniformLocation = glGetUniformLocation(mShaderProgram, uniformName);
	glUniform4f(uniformLocation, color.r, color.g, color.b, color.a);
}

void Shader::setBufferObjectData(float *data, unsigned int size)
{
	glBindVertexArray(mVao);
	glBindBuffer(GL_ARRAY_BUFFER, mVbo);
	glBufferData(GL_ARRAY_BUFFER, size, data, GL_STREAM_DRAW);
}

void Shader::use()
{
	glBindVertexArray(mVao);
	glUseProgram(mShaderProgram);
}

Shader::~Shader()
{
	glDeleteProgram(mShaderProgram);
	glDeleteShader(mFragmentShader);
	glDeleteShader(mVertexShader);

	glDeleteBuffers(1, &mVao);
	glDeleteBuffers(1, &mVbo);
	glDeleteBuffers(1, &mEbo);
}


// Default shaders to perform simple tasks (since you are forced to use shaders in newer version of openGL)
Shader DefaultShader::sprite;
Shader DefaultShader::renderTexture;
Shader DefaultShader::rectangle;

DefaultShader::DefaultShader()
{
	// Sprite shader that is used for 2D images
	phx::ShaderCode spriteVertexShaderCode = 
	GLSL_330
	(
		in vec2 position;
		in vec2 texcoord;

		out vec2 Texcoord;

		uniform vec2 spritePosition;
		uniform vec2 spriteSize;

		void main()
		{
			Texcoord = texcoord;
			gl_Position = vec4(position.x*spriteSize.x - (1.0 - spriteSize.x) + spritePosition.x, position.y*spriteSize.y + (1.0 - spriteSize.y) - spritePosition.y, 0.0, 1.0);
		}
	);
	phx::ShaderCode spriteFragmentShaderCode = 
	GLSL_330
	(
		in vec2 Texcoord;
		out vec4 outColor;

		uniform sampler2D spriteTexture;

		void main()
		{
			outColor = texture(spriteTexture, Texcoord);
		}
	);
	DefaultShader::sprite.loadFromMemory(spriteVertexShaderCode, spriteFragmentShaderCode);

	// RenderTexture shader that is used for drawing scene to texture
	phx::ShaderCode renderTextureVertexShaderCode = 
	GLSL_330
	(
		in vec2 position;
		in vec2 texcoord;

		out vec2 Texcoord;

		void main() 
		{
			Texcoord = texcoord;
			gl_Position = vec4(position, 0.0, 1.0);
		}
	);
	phx::ShaderCode renderTextureFragmentShaderCode = 
	GLSL_330
	(
		in vec2 Texcoord;
		out vec4 outColor;

		uniform sampler2D texFramebuffer;

		void main() 
		{
			//outColor = texture(texFramebuffer, Texcoord);
			float FXAA_SPAN_MAX = 8.0;
			float FXAA_REDUCE_MUL = 1.0/8.0;
			float FXAA_REDUCE_MIN = 1.0/128.0;

			vec2 frameBufSize = vec2(1280.0, 768.0);

			vec3 rgbNW=texture2D(texFramebuffer,Texcoord+(vec2(-1.0,-1.0)/frameBufSize)).xyz;
			vec3 rgbNE=texture2D(texFramebuffer,Texcoord+(vec2(1.0,-1.0)/frameBufSize)).xyz;
			vec3 rgbSW=texture2D(texFramebuffer,Texcoord+(vec2(-1.0,1.0)/frameBufSize)).xyz;
			vec3 rgbSE=texture2D(texFramebuffer,Texcoord+(vec2(1.0,1.0)/frameBufSize)).xyz;
			vec3 rgbM=texture2D(texFramebuffer,Texcoord).xyz;
        
			vec3 luma=vec3(0.299, 0.587, 0.114);
			float lumaNW = dot(rgbNW, luma);
			float lumaNE = dot(rgbNE, luma);
			float lumaSW = dot(rgbSW, luma);
			float lumaSE = dot(rgbSE, luma);
			float lumaM  = dot(rgbM,  luma);
        
			float lumaMin = min(lumaM, min(min(lumaNW, lumaNE), min(lumaSW, lumaSE)));
			float lumaMax = max(lumaM, max(max(lumaNW, lumaNE), max(lumaSW, lumaSE)));
        
			vec2 dir;
			dir.x = -((lumaNW + lumaNE) - (lumaSW + lumaSE));
			dir.y =  ((lumaNW + lumaSW) - (lumaNE + lumaSE));
        
			float dirReduce = max(
					(lumaNW + lumaNE + lumaSW + lumaSE) * (0.25 * FXAA_REDUCE_MUL),
					FXAA_REDUCE_MIN);
          
			float rcpDirMin = 1.0/(min(abs(dir.x), abs(dir.y)) + dirReduce);
        
			dir = min(vec2( FXAA_SPAN_MAX,  FXAA_SPAN_MAX),
					  max(vec2(-FXAA_SPAN_MAX, -FXAA_SPAN_MAX),
					  dir * rcpDirMin)) / frameBufSize;
                
			vec3 rgbA = (1.0/2.0) * (
					texture2D(texFramebuffer, Texcoord.xy + dir * (1.0/3.0 - 0.5)).xyz +
					texture2D(texFramebuffer, Texcoord.xy + dir * (2.0/3.0 - 0.5)).xyz);
			vec3 rgbB = rgbA * (1.0/2.0) + (1.0/4.0) * (
					texture2D(texFramebuffer, Texcoord.xy + dir * (0.0/3.0 - 0.5)).xyz +
					texture2D(texFramebuffer, Texcoord.xy + dir * (3.0/3.0 - 0.5)).xyz);
			float lumaB = dot(rgbB, luma);

			if((lumaB < lumaMin) || (lumaB > lumaMax)){
					outColor = vec4(rgbA, 1.0);
			}else{
					outColor = vec4(rgbB, 1.0);
			}
		}
	);
	DefaultShader::renderTexture.loadFromMemory(renderTextureVertexShaderCode, renderTextureFragmentShaderCode);

	// Rectangle shader for drawing colored rectangles
	phx::ShaderCode rectangleVertexShaderCode =
	GLSL_330
	(
		in vec2 position;

		uniform vec2 rectanglePosition;
		uniform vec2 rectangleSize;

		void main()
		{
			gl_Position = vec4(position.x*rectangleSize.x - (1.0 - rectangleSize.x) + rectanglePosition.x, position.y*rectangleSize.y + (1.0 - rectangleSize.y) - rectanglePosition.y, 0.0, 1.0);
		}
	);
	phx::ShaderCode rectangleFragmentShaderCode =
	GLSL_330
	(
		uniform vec4 color;
		out vec4 outColor;

		void main()
		{
			outColor = color;
		}
	);
	DefaultShader::rectangle.loadFromMemory(rectangleVertexShaderCode, rectangleFragmentShaderCode);
	/*
	phx::ShaderCode fontVertexShaderCode =
	GLSL_330
	(
		in vec4 coord;
		out vec2 texcoord;

		void main()
		{
			gl_Position = vec4(coord.xy, 0.0, 1.0);
			texcoord = coord.zw;
		}
	);
	phx::ShaderCode fontFragmentShaderCode =
	GLSL_330
	(
		in vec2 texcoord;
		uniform sampler2D tex;
		uniform vec4 color;

		out vec4 outColor;

		void main()
		{
			outColor = vec4(1.0, 1.0, 1.0, texture2D(tex, texcoord).a) * color;
		}
	);
	DefaultShader::font.loadFromMemory(fontVertexShaderCode, fontFragmentShaderCode);
	*/
}

}